﻿using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Windows.ApplicationModel;
using Windows.Management.Deployment;
using Windows.Storage;
using Windows.UI.Notifications;

namespace CorpStoreServer
{
    /// <summary>
    /// This class serves the function of creating a master catalog JSON file from
    /// a combination of sources including commonCatalog.json (the user's list of 
    /// authorized apps), the Windows Store web service (to obtain store app 
    /// attributes), previously installed store apps from the publisher defined in
    /// 'Settings' section of commonCatalog.json, and embedded icons in win32 
    /// executables.  The master catalog JSON file is required for the operation 
    /// of CorpStore store app since it is this master catalog which gets deserialized 
    /// to be the master list of apps presented in the UI.  
    /// </summary>
    class UpdateMasterCatalog
    {
        private Log _log;
        private List<Package> _allInstalledPackages;

        // updateMasterCatalog 

        /// <summary>
        /// This function attempts to create the following files in the 'LocalState'
        /// directory of the CorpStore app:
        /// 
        /// \
        ///     MasterCatalog.json (serialized CorpGroupList)
        /// \AppFiles
        ///     *_logo.png
        ///     *_screenshot.png
        ///     updateTile.trigger (trigger file to trigger update of CorpStore tile)
        /// \Toasts
        ///     lastFeatured.json (List<CorpApp>)
        ///     *_toast.xml (gets deleted after each is toasted to avoid re-toasting!  existence of any is trigger for tile update as well)
        /// 
        /// mode=quiet (optional parm to disable console when updating master)
        /// </summary>
        /// <param name="parms"></param>
        public void Run(Log log, NameValueCollection parms, AppSettings appSettings, bool showConsole = true)
        {
            string mode = GenLib.GetParm(parms, "mode");
            string commonCatalogFn = appSettings.CommonCatalogFn;
            PackageInfoGroupList installedApps;
            PackageInfoGroupList commonApps;
            string errStr;

            // show console window when rebuilding master catalog if necessary
            if (showConsole && mode == "")
            {
                logInfo("About to show console window...");
                GenLib.ShowConsoleWindow();
            }
            else
                logInfo("Not showing console window; mode = '{0}'", mode);

            this._log = log;
            logInfo("At top of UpdateMasterCatalog.Run; appSettings.commonCatalogFn = '{0}', mode = '{1}'", commonCatalogFn, mode);

            // override commonCatalogFn if necessary
            string overrideFn = GenLib.GetParm(parms, "CommonCatalogFn");
            if (overrideFn != "")
            {
                logInfo("Overriding common catalog filename contained in appsettings.json file with passed in one of '{0}'", overrideFn);
                commonCatalogFn = overrideFn;
            }

            // set default commonCatalog position if not supplied
            if (string.IsNullOrEmpty(commonCatalogFn))
            {
                commonCatalogFn = string.Format(@"{0}\CorpStoreServer\commonCatalog.json", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create));

                // check if common catalog even exists in the applicationdata folder
                if (File.Exists(commonCatalogFn))
                    logInfo("Confirmed that common catalog file '{0}' exists", commonCatalogFn);
                else
                {
                    // attempt to find common catalog file in same directory as this exe
                    logInfo("Could not find common catalog file at '{0}'", commonCatalogFn);
                    commonCatalogFn = string.Format(@"{0}\commonCatalog.json", GenLib.AppExePath());
                }

                logInfo("CommonCatalogFn parameter not supplied - using default of {0}", commonCatalogFn);
            }

            if (commonCatalogFn != "")
            {
                // command line parms supplied - verify that our commonCatalog.json file exists
                if (System.IO.File.Exists(commonCatalogFn))
                {
                    // attempt to load all installed packages for user since we will use this list during this process
                    logInfo("About to load all packages for user into memory");
                    if (GenLib.LoadAllPackagesForUser(out _allInstalledPackages, out errStr))
                    {
                        // store catalog file exists - now check if our CorpStore package has been installed
                        logInfo("Found {0} apps to be installed for current user", _allInstalledPackages.Count);
                        Package corpStorePkg = findPackageByFamilyName(_allInstalledPackages, Main.STORE_FAMILY_NAME);
                        if (corpStorePkg != null)
                        {
                            // extract images from packages directory into our Local\Images directory
                            string corpStoreLocalPath = string.Format(@"{0}\Packages\{1}\LocalState",
                                                                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                                                                corpStorePkg.Id.FamilyName);
                            string toastsPath = string.Format(@"{0}\Toasts", corpStoreLocalPath);
                            string appFilesPath = string.Format(@"{0}\AppFiles", corpStoreLocalPath);
                            string masterCatalogPath = string.Format(@"{0}\", corpStoreLocalPath);

                            // create destination directories 
                            Directory.CreateDirectory(toastsPath);
                            Directory.CreateDirectory(appFilesPath);

                            // loaded installed apps ok - now load common apps info 
                            if (loadCommonGroups(commonCatalogFn, appFilesPath, out commonApps))
                            {
                                // Corp Store pkg exists - attempt to load all installed packages for our publisher, excluding the CorpStore pkg itself
                                if (commonApps.Settings.ContainsKey("corpPublisherDisplayName"))
                                {
                                    string corpPublisherCN = commonApps.Settings["corpPublisherDisplayName"];
                                    logInfo("Publisher display name defined in '{0}' is '{1}'", commonCatalogFn, corpPublisherCN);
                                    if (findInstalledAppsForPublisher(corpPublisherCN, corpStorePkg.Id.FamilyName, out installedApps))
                                    {
                                        // combine installsApps with commonApps
                                        PackageInfoGroupList combinedGroups = combineAppGroups(installedApps, commonApps);

                                        // now extract image files for all apps in all groups 
                                        if (extractImagesForApps(combinedGroups, appFilesPath))
                                        {
                                            // extracted images ok - now create toast files if needed
                                            createToastsFiles(toastsPath, installedApps.FeaturedApps);

                                            // and serialize combined groups into our MasterCatalog.json file
                                            string masterFn = string.Format(@"{0}\MasterCatalog.json", masterCatalogPath);
                                            if (createCorpCatalog(masterFn, combinedGroups))
                                            {
                                                logInfo("Updated Corp Catalog {0} OK!", masterFn);
                                            }
                                            else
                                                logErr("Error creating Corp Catalog {0}", masterFn);
                                        }
                                        else
                                            logErr("Error extracting images for all combined apps");
                                    }
                                    else
                                        logErr("Error loading installed app group for publisher '{0}'", corpPublisherCN);
                                }
                                else
                                    logErr("The corporate publisher display name 'corpPublisherDisplayName' has not been defined in the Settings section of the common apps file '{0}'; this must be defined.", commonCatalogFn);
                            }
                            else
                                logErr("Error loading common group apps from common catalog '{0}'", commonCatalogFn);
                        }
                        else
                            logErr("Cannot find corpStore package.  CorpStore app must be installed before running CorpStoreServer");
                    }
                    else
                        logErr("Error loading all installed packages for our current user");
                }
                else
                    logErr("Common catalog file '{0}' does not exist - cannot update our Corp catalog.  Check the filename and try again.", commonCatalogFn);
            }
            else
                logErr("One or more of the 'CorpPublisherCN' and 'commonCatalogPath' parameters are blank.  Please define them and try again.");


            if (mode.Equals("launchApp", StringComparison.OrdinalIgnoreCase))
            {
                // need to relaunch store app now
                logInfo("Relaunch store app at end of updateMasterCatalog since mode was 'launchApp'");
                StartEnterpriseApp obj = new StartEnterpriseApp();
                parms.Clear();
                parms.Add("familyName", Main.STORE_FAMILY_NAME);
                obj.Run(log, parms);
            }
        }


        private bool extractImagesForApps(PackageInfoGroupList combinedGroups, string destPath)
        {
            bool blnOK = true;

            //// first delete all files in the destPath so we don't combine new files with old ones
            //if (GenLib.deleteFilesInDirectory(destPath, "*.*", out errStr))
            //    logInfo("Deleted all files in images directory '{0}' OK", destPath);
            //else
            //    logErr("Error deleting all files in images directory '{0}'", destPath);
            logInfo("At top of extractImagesForApps");
            foreach (PackageInfoGroup group in combinedGroups.Groups)
            {
                logInfo("Attempting to extract images for apps in group '{0}'", group.Name);
                blnOK &= extractImagesForAppGroup(group, destPath);
            }
            logInfo("At bottom of extractImagesForApps; blnOK = {0}", blnOK.ToString());
            return blnOK;
        }


        private bool extractImagesForAppGroup(PackageInfoGroup group, string destPath)
        {
            bool blnOK = false;

            foreach (PackageInfo app in group.Applications)
            {
                switch (app.AppType())
                {
                    case eAppType.enterpriseApp:
                        blnOK = extractEnterpriseImages(app, destPath);
                        break;
                    case eAppType.storeApp:
                        blnOK = extractStoreImages(app, destPath);
                        break;
                    case eAppType.win32App:
                        blnOK = extractWin32Images(app, destPath);
                        break;
                }
            }

            return blnOK || group.Applications.Count == 0;
        }


        /// <summary>
        /// this function attempts to extract the logo and screenshot images from
        /// the previously installed package or from the appx file directly, depending
        /// on whether the package can be found as installed.
        /// </summary>
        /// <param name="app"></param>
        /// <returns></returns>
        private bool extractEnterpriseImages(PackageInfo app, string destPath)
        {
            bool blnOK = true;
            string strErr;
            string destFn;

            // first check if app has already been installed 
            Package pkg = findPackageByAppName(_allInstalledPackages, app.Name);

            if (pkg != null)
            {
                // found this package to be already installed - just copy the logo and screenshot to our dest path
                app.InstalledLocation = pkg.InstalledLocation.Path;

                // copy logo 
                if (!string.IsNullOrEmpty(app.LogoFn))
                    blnOK &= copyPackageFile(app.InstalledLocation, app.LogoFn, destPath, app.Name + "_logo.png");
                else
                    logInfo("Not copying logo for package '{0}' since no logo has been defined in its appxmanifest.xml file", app.Name);

                // copy splashscreen
                if (!string.IsNullOrEmpty(app.SplashScreenFn))
                    blnOK &= copyPackageFile(app.InstalledLocation, app.SplashScreenFn, destPath, app.Name + "_splashscreen.png");
                else
                    logInfo("Not copying splashscreen for package '{0}' since no splashscreen has been defined in its appxmanifest.xml file", app.Name);

                for (int i = 0; i <= app.Screenshots.Count - 1; i++)
                {
                    ScreenshotInfo screenshotInfo = app.Screenshots[i];
                    blnOK &= copyPackageFile(app.InstalledLocation, screenshotInfo.Filename, destPath, string.Format(@"{0}_screenshot{1}.png", app.Name, i));
                }
            }
            else
            {
                // package not yet installed - we will need to extract from the appx package file itself - check if our app has the appxFn defined 
                if (!string.IsNullOrEmpty(app.AppxFn))
                {
                    // appxFn was defined - extract logo first 
                    destFn = string.Format("{0}_logo.png", app.Name);
                    if (GenLib.ExtractZipFileToDirectory(app.AppxFn, app.LogoFn.Replace("\\", "/"), destPath, destFn, out strErr))
                        logInfo("Extracted logo {0} for app {1} to {2} OK at destPath {3}", app.LogoFn, app.Name, destFn, destPath);
                    else
                    {
                        logErr("Error extracting logo {0} for app {1} to {2}: {3}", app.LogoFn, app.Name, destFn, strErr);
                        blnOK = false;
                    }

                    // now extract splashscreen 
                    destFn = string.Format("{0}_splashscreen.png", app.Name);
                    if (GenLib.ExtractZipFileToDirectory(app.AppxFn, app.SplashScreenFn.Replace("\\", "/"), destPath, destFn, out strErr))
                        logInfo("Extracted splashscreen {0} for app {1} OK", app.SplashScreenFn, app.Name);
                    else
                    {
                        logErr("Error extracting splashscreen {0} for app {1}: {2}", app.SplashScreenFn, app.Name, strErr);
                        blnOK = false;
                    }

                    // now extract screenshot(s)
                    for (int i = 0; i <= app.Screenshots.Count - 1; i++)
                    {
                        ScreenshotInfo screenshotInfo = app.Screenshots[i];
                        destFn = string.Format(@"{0}_screenshot{1}.png", app.Name, i);
                        if (GenLib.ExtractZipFileToDirectory(app.AppxFn, screenshotInfo.Filename.Replace("\\", "/"), destPath, destFn, out strErr))
                            logInfo("Extracted screenshot #{0} called '{1}' for app {2} OK", i, screenshotInfo.Filename, app.Name);
                        else
                        {
                            logErr("Error extracting screenshot #{0} called '{1}' for app {2}: {3}", i, screenshotInfo.Filename, app.Name, strErr);
                            blnOK = false;
                        }
                    }
                }
            }

            return blnOK;
        }


        private bool extractStoreImages(PackageInfo app, string destPath)
        {
            bool blnOK = true;
            string destFn;

            // extract logo first
            destFn = string.Format(@"{0}\{1}_logo.png", destPath, app.Name);
            blnOK &= extractStoreImage(app.LogoFn, destFn);

            // extract splashscreen next
            destFn = string.Format(@"{0}\{1}_splashscreen.png", destPath, app.Name);
            blnOK &= extractStoreImage(app.SplashScreenFn, destFn);

            // extract screenshots next
            for (int i = 0; i <= app.Screenshots.Count - 1; i++)
            {
                ScreenshotInfo screenshotInfo = app.Screenshots[i];
                destFn = string.Format(@"{0}\{1}_screenshot{2}.png", destPath, app.Name, i);
                blnOK &= extractStoreImage(screenshotInfo.Filename, destFn);
            }

            return blnOK;
        }


        private bool extractWin32Images(PackageInfo app, string destPath)
        {
            bool blnOK = true;
            string destFn;

            // extract logo first
            destFn = string.Format(@"{0}\{1}_logo.png", destPath, app.Name);
            blnOK &= extractWin32Image(app.LogoFn, app.ExeName, destFn);

            // extract splashscreen next
            destFn = string.Format(@"{0}\{1}_splashscreen.png", destPath, app.Name);
            blnOK &= extractWin32Image(app.SplashScreenFn, app.ExeName, destFn);

            // extract screenshots next
            for (int i = 0; i <= app.Screenshots.Count - 1; i++)
            {
                ScreenshotInfo screenshotInfo = app.Screenshots[i];
                if (!string.IsNullOrEmpty(screenshotInfo.Filename))
                {
                    destFn = string.Format(@"{0}\{1}_screenshot{2}.png", destPath, app.Name, i);
                    blnOK &= extractWin32Image(screenshotInfo.Filename, app.ExeName, destFn);
                }
                else
                    logInfo("Not attempting to copy screenshot for win32 app {0} since no screenshotFn has been defined in commonCatalog.json", app.Name);
            }
            return blnOK;
        }




        private bool findInstalledAppsForPublisher(string publisherCN, string familyNameToExclude, out PackageInfoGroupList publisherPackages)
        {
            bool blnOK = false;
            string errStr;

            publisherPackages = new PackageInfoGroupList();
            try
            {
                int numFound = 0;

                foreach (Package pkg in _allInstalledPackages)
                {
                    string currPublisher = parseCN(pkg.Id.Publisher);
                    if (!pkg.IsFramework && (publisherCN == "" || currPublisher.Equals(publisherCN, StringComparison.OrdinalIgnoreCase)) &&
                        (familyNameToExclude == "" || !pkg.Id.FamilyName.Equals(familyNameToExclude, StringComparison.OrdinalIgnoreCase)))
                    {
                        // we found a package for the publisher we are interested in - include it in our list of packages
                        numFound++;
                        PackageInfo info = new PackageInfo();
                        if (info.Init(pkg, out errStr))
                            publisherPackages.AddApplication(info);
                        else
                            logErr("Error initializing package info for package '{0}': {1}", pkg.Id.Name, errStr);
                    }
                }
                logInfo("Found {0} packages for publisher {1}.", numFound, publisherCN);

                // sort found packages list by date (assume date is left most data in each string)
                publisherPackages.SortGroupsByName();

                blnOK = true;
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in findInstalledAppsForPublisher: {0}", ex.Message);
            }

            return blnOK;
        }


        private string parseCN(string DN)
        {
            int cnPos = DN.IndexOf("CN=", 0, StringComparison.OrdinalIgnoreCase);
            string cn = "";

            if (cnPos != -1)
            {
                int commaPos = DN.IndexOf(",", cnPos);
                if (commaPos == -1) commaPos = DN.Length;
                cn = DN.Substring(cnPos + 3, cnPos + commaPos - 3);
            }
            return cn;
        }


        /// <summary>
        /// This function scans 
        /// </summary>
        /// <param name="toastsPath"></param>
        /// <param name="installedApps">List of all previously installed apps from publisher provided on URL/command line</param>
        /// <returns></returns>
        private bool createToastsFiles(string toastsPath, List<PackageInfo> featuredApps)
        {
            bool blnOK = true;
            string errStr;
            string lastFeaturedFn = string.Format(@"{0}\lastFeatured.json", toastsPath);
            List<PackageInfo> lastFeaturedList = null;

            // check if our last toasted file exists
            if (File.Exists(lastFeaturedFn))
            {
                // our last toasted file exists - read it and deserialize it
                string rawStr = System.IO.File.ReadAllText(lastFeaturedFn);
                lastFeaturedList = JsonConvert.DeserializeObject<List<PackageInfo>>(rawStr);
            }

            // delete all existing files in toasts directory now
            if (GenLib.deleteFilesInDirectory(toastsPath, "*.*", out errStr))
                logInfo("Deleted all files in toast directory '{0}' OK", toastsPath);
            else
                logErr("Error deleting all files in toast directory '{0}'", toastsPath);

            // we loaded our last featured list ok - now compare it with the current featuredlist
            foreach (PackageInfo app in featuredApps)
            {
                if (lastFeaturedList == null || lastFeaturedList.SingleOrDefault(a => a.Name.Equals(app.Name, StringComparison.OrdinalIgnoreCase)) == null)
                {
                    // this is a newly featured app - create toast for it
                    blnOK &= createToastForPackage(toastsPath, app);
                }
            }

            // always include at least one toast saying that new apps may be available
            createToastForAppsUpdated(toastsPath);

            // always write our last featured list file
            string serializedData = JsonConvert.SerializeObject(featuredApps, Newtonsoft.Json.Formatting.Indented);
            System.IO.File.WriteAllText(lastFeaturedFn, serializedData);

            return blnOK;
        }


        /// <summary>
        /// This function will create a 'toast-<familyName>.xml' file for package 'pkg'
        /// in the 'imagePath' directory.  The image the xml file references is
        /// assumed to be named '<familyName>.png', where <familyName> is the package family 
        /// name of 'pkg'.  The text shown on the tile includes the 'Name' as
        /// defined within 'pkg.Id'.
        /// </summary>
        /// <param name="toastsPath"></param>
        /// <param name="pkg"></param>
        /// <returns></returns>
        private bool createToastForPackage(string toastsPath, PackageInfo info)
        {
            return createToast(toastsPath, info.Name, string.Format("{0} is available!", info.DisplayName), "Click here to view app details...",
                                string.Format("ms-appdata:///Local/AppFiles/{0}_logo.png", info.Name), info.FamilyName);
        }


        private bool createToastForAppsUpdated(string toastsPath)
        {
            return createToast(toastsPath, "AppsUpdated", "Apps have been updated", "Click here to view app store", "ms-appx:///Assets/CorpStore 50x50.png", "");
        }


        /// <summary>
        /// This function will create a 'toast-<familyName>.xml' file for package 'pkg'
        /// in the 'imagePath' directory.  The image the xml file references is
        /// assumed to be named '<familyName>.png', where <familyName> is the package family 
        /// name of 'pkg'.  The text shown on the tile includes the 'Name' as
        /// defined within 'pkg.Id'.
        /// </summary>
        /// <param name="toastsPath"></param>
        /// <param name="pkg"></param>
        /// <returns></returns>
        private bool createToast(string toastsPath, string toastName, string text1, string text2, string imageFn, string launchParm)
        {
            bool blnOK = false;

            try
            {
                ToastTemplateType template = ToastTemplateType.ToastImageAndText02;
                var xml = ToastNotificationManager.GetTemplateContent(template);

                // set text to display
                var textNodes = xml.GetElementsByTagName("text");
                if (textNodes.Count == 2)
                {
                    textNodes[0].AppendChild(xml.CreateTextNode(text1));
                    textNodes[1].AppendChild(xml.CreateTextNode(text2));
                }

                // set image element
                var imageNode = xml.GetElementsByTagName("image").FirstOrDefault();
                ((Windows.Data.Xml.Dom.XmlElement)imageNode).SetAttribute("src", imageFn);

                // set launch attribute
                var toastNode = xml.SelectSingleNode("/toast");
                ((Windows.Data.Xml.Dom.XmlElement)toastNode).SetAttribute("launch", launchParm);

                // now save xml to file
                Directory.CreateDirectory(toastsPath);
                string destFn = string.Format(@"{0}\{1}_toast.xml", toastsPath, toastName);
                File.WriteAllText(destFn, xml.GetXml());

                logInfo("Created toast xml file for toast '{0}' at '{1}' OK", toastName, destFn);
                blnOK = true;
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception while creating toast for toast {0} : {1}", toastName, ex.Message);
            }

            return blnOK;
        }




        private bool copyPackageFile(string appxPath, string packageFn, string destPath, string overridenFn = "")
        {
            bool blnOK = false;

            try
            {
                // we could use ms-appx://<logoFn> here as well
                string sourceFn = string.Format(@"{0}\{1}", appxPath, packageFn);
                if (File.Exists(sourceFn))
                {
                    string destFn = string.Format(@"{0}\{1}", destPath, overridenFn == "" ? Path.GetFileName(packageFn) : overridenFn);
                    Directory.CreateDirectory(Path.GetDirectoryName(destFn));
                    File.Copy(sourceFn, destFn, true);
                    logInfo("Copied package file '{0}' to dest file '{1}' from appx package located at '{2}' OK", sourceFn, destFn, appxPath);
                    blnOK = true;
                }
                else
                    logErr("Cannot copy package file '{0}' since it does not exist", sourceFn);
            }
            catch (Exception ex)
            {
                logErr("Unhandled exceptionin copyPackageFile: {0}", ex.Message);
            }

            return blnOK;
        }


        private bool loadCommonGroups(string commonCatalogFn, string appFilesPath, out PackageInfoGroupList commonAppGroups)
        {
            bool blnOK = true;
            List<PackageInfo> packagesToRemove = new List<PackageInfo>();

            commonAppGroups = null;
            try
            {
                string rawStr = System.IO.File.ReadAllText(commonCatalogFn);
                commonAppGroups = JsonConvert.DeserializeObject<PackageInfoGroupList>(rawStr);

                foreach (PackageInfoGroup group in commonAppGroups.Groups)
                {
                    packagesToRemove.Clear();
                    for (int i = 0; i < group.Applications.Count; i++)
                    {
                        PackageInfo app = group.Applications[i];
                        switch (app.AppType())
                        {
                            case eAppType.storeApp:
                                // common app is a store app - obtain its image from the web
                                if (getStoreAppInfo(app.StoreID, ref app))
                                {
                                    // found app info ok - check if store app is already installed so that we can get install date/location 
                                    Package pkg = findPackageByFamilyName(_allInstalledPackages, app.FamilyName);
                                    if (pkg != null)
                                    {
                                        app.InstalledLocation = pkg.InstalledLocation.Path;
                                        app.CreateDate = pkg.InstalledLocation.DateCreated.ToString("u");

                                        // check if needs updating
                                        if (app.Version != null)
                                        {
                                            if (app.Version.Equals(pkg.Id.Version))
                                            {
                                                logInfo("Store app '{0}' is installed AND up to date with version {1}", app.DisplayName, app.Version);
                                                app.NeedsUpdate = false;
                                            }
                                            else
                                            {
                                                logInfo("Store app '{0}' is installed BUT out of date; current version = {1} but store version = {2}", app.DisplayName, app.Version, pkg.Id.Version);
                                                app.NeedsUpdate = true;
                                            }
                                        }
                                        else
                                        {
                                            logInfo("Cannot determine if store app is up to date since we were unable to retrieve version info from the store web service");
                                            app.NeedsUpdate = false;
                                        }
                                    }

                                    group.Applications[i] = app;
                                }
                                else
                                {
                                    logErr("Error obtaining store app info from web for store app {0}; make sure this value matches the value from the Share Charm for this app in the Windows Store", app.StoreID);
                                    packagesToRemove.Add(app);
                                }
                                break;

                            case eAppType.win32App:
                                // a win32 app - we don't have any other source of info for a win32 app other than commonCatalog.json - use as is

                                if (File.Exists(app.ExeName))
                                {
                                    // set our displayName = name if it was not defined by user in common catalog; we need to do this since we are sorting on displayName
                                    if (string.IsNullOrEmpty(app.Name)) app.Name = app.DisplayName;
                                    if (string.IsNullOrEmpty(app.SplashScreenFn)) app.SplashScreenFn = app.LogoFn;
                                    app.InstalledLocation = app.ExeName;
                                    app.CreateDate = Directory.GetCreationTime(app.ExeName).ToString();
                                    app.Version = FileVersionInfo.GetVersionInfo(app.ExeName).ProductVersion;
                                    app.FamilyName = app.ExeName.Replace("\\", "_"); // set familyName = exename to have a unique ID for this win32 app
                                    app.GroupName = group.Name;

                                    // define features
                                    if (app.Features == null || app.Features.Count == 0)
                                        app.Features.Add("Legacy Windows Application");

                                    // define screenshots
                                    if (app.Screenshots == null || app.Screenshots.Count == 0)
                                        app.Screenshots.Add(new ScreenshotInfo() { Description = "Screenshot not available", Filename = app.SplashScreenFn });
                                }
                                else
                                {
                                    logErr("Cannot include win32 package since the exeName '{0}' cannot be found", app.ExeName);
                                    packagesToRemove.Add(app);
                                }
                                break;

                            case eAppType.enterpriseApp:
                                // is a enterprise app - attempt to get package info from manifest file in appx package
                                if (!string.IsNullOrEmpty(app.AppxFn))
                                {
                                    // expand appxFn so that we parse embedded environment vars properly
                                    string strErr;
                                    app.AppxFn = GenLib.ExpandEnvironmentVars(app.AppxFn, out strErr);

                                    // appxFn is defined - now get package info from appx file itself
                                    if (getPackageInfoFromAppx(app.AppxFn, ref app))
                                    {
                                        // got package info ok 
                                        group.Applications[i] = app;
                                        logInfo("Obtained enterprise app info OK, name = '{0}', logoFn = '{1}', num screenshots = '{2}'", app.Name, app.LogoFn, app.Screenshots.Count);
                                    }
                                    else
                                    {
                                        logErr("Could not determine package info from appx file '{0}' - not including package", app.AppxFn);
                                        packagesToRemove.Add(app);
                                    }
                                }
                                else
                                {
                                    logErr("Skipping common enterprise app '{0}' since appxFn attribute is not defined.", app.Name);
                                    packagesToRemove.Add(app);
                                }
                                break;
                        } // switch appType
                    } // scanning all apps in group

                    // remove all packages we couldn't resolve
                    logInfo("Collected {0} packages to remove due to previous errors", packagesToRemove.Count);
                    foreach (PackageInfo app in packagesToRemove)
                        group.Applications.Remove(app);

                    // sort all apps in this group before moving to next group
                    logInfo("About to sort all applications in group {0} by name; num apps in group = {1}", group.Name, group.Applications.Count);
                    group.SortApplicationsByName();
                } // scanning all groups
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in loadCommonGroups: {0}", ex.Message);
                blnOK = false;
            }

            return blnOK;
        }


        private bool findPackage(List<Package> allInstalledPackages, string familyName, out PackageInfo info)
        {
            bool blnOK = false;
            string errStr;

            info = new PackageInfo();
            try
            {
                Package pkg = allInstalledPackages.SingleOrDefault(p => p.Id.FamilyName.Equals(familyName, StringComparison.OrdinalIgnoreCase));
                if (pkg != null)
                {
                    // found a package with that familyName - convert package info to our PackageInfo structure
                    blnOK = info.Init(pkg, out errStr);
                }
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in findPackage with familyName = '{0}': {1}", familyName, ex.Message);
            }

            return blnOK;
        }


        private bool extractWin32Image(string sourceFn, string exeName, string destFn)
        {
            bool blnOK = false;

            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(destFn));

                if (File.Exists(sourceFn))
                {
                    File.Copy(sourceFn, destFn, true);
                    logInfo("Copied win32 logo '{0}' to '{1}' OK", sourceFn, destFn);
                }
                else
                {
                    logInfo("Image file '{0}' not found - attempting to extract from exe file '{1}'", sourceFn, exeName);
                    IconExtractor extractor = new IconExtractor(exeName, IconSize.Large);
                    if (extractor.Count > 0)
                    {
                        Icon icon = extractor[0];
                        using (FileStream fs = new FileStream(destFn, FileMode.Create))
                            icon.Save(fs);
                        logInfo("Extracted {0}x{1} icon from exe file '{2}' to '{3}' OK", icon.Width, icon.Height, exeName, destFn);
                    }
                    else
                        logInfo("Could not find any embedded logos in '{0}'", exeName);
                }

                blnOK = true;
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in copyWin32LogoToCorpStoreDirectory while copying {0} to destination file {1}: {2}", sourceFn, destFn, ex.Message);
            }

            return blnOK;
        }


        private bool extractStoreImage(string sourceImageFn, string destFn)
        {
            bool blnOK = false;

            try
            {
                if (!File.Exists(destFn))
                {
                    logInfo("Attempting to download store image {0} to dest file {1}...", sourceImageFn, destFn);
                    HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("http://wscont1.apps.microsoft.com/winstore/1x/" + sourceImageFn);

                    // returned values are returned as a stream, then read into a string
                    using (HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse())
                    {
                        using (BinaryReader reader = new BinaryReader(response.GetResponseStream()))
                        {
                            Byte[] lnByte = reader.ReadBytes(1 * 1024 * 1024 * 10);
                            using (FileStream fs = new FileStream(destFn, FileMode.Create))
                            {
                                fs.Write(lnByte, 0, lnByte.Length);
                            }

                            logInfo("Copied downloaded store logo to destination file '{0}' OK", destFn);
                            blnOK = true;
                        }
                    }
                }
                else
                {
                    logInfo("Store image '{0}' already exists - not downloading again!", destFn);
                    blnOK = true;
                }
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in extractStoreImage with sourceImageFn '{0}', destFn '{1}': {2}",
                    sourceImageFn, destFn, ex.Message);
            }

            return blnOK;
        }


        private bool getPackageInfoFromAppx(string appxFn, ref PackageInfo info)
        {
            bool blnOK = false;
            string strErr;
            string destPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData);
            string destFn = string.Format(@"{0}\appxmanifest.xml", destPath);
            string priFn = string.Format(@"{0}\resources.pri", destPath);

            if (GenLib.ExtractZipFileToDirectory(appxFn, new List<string>() { "appxmanifest.xml", Path.GetFileName(priFn) }, destPath, "", out strErr))
            {
                info.AppxFn = appxFn; // = new PackageInfo() { AppxFn = appxFn };

                // extracted manifest file ok - read it
                XmlDocument xml = new XmlDocument();
                xml.Load(destFn);

                info.AppxFn = appxFn;

                info.Name = GenLib.parseElement(xml, "Identity", "Name", out strErr);
                info.Identity = GenLib.parseElement(xml, "Application", "Id", out strErr);
                info.Version = GenLib.parseElement(xml, "Identity", "Version", out strErr);
                info.CreateDate = GenLib.GetDirectoryDate(Path.GetDirectoryName(appxFn)).ToString();

                info.DisplayName = GenLib.GetResourcedElementValue(priFn, info.Name, xml, "DisplayName", "", out strErr);

                // derive family name from displayName (need a better way to determine familyName given an appx file)
                info.FamilyName = GenLib.getFamilyNameForDisplayName(info.DisplayName);
                if (string.IsNullOrEmpty(info.FamilyName))
                    logErr("Cannot determine familyName for package at appxFn '{0}'", appxFn);
                
                info.Description = GenLib.GetResourcedElementValue(priFn, info.Name, xml, "VisualElements", "Description", out strErr);
                info.LogoFn = getFnWithScaleFromZip(appxFn, GenLib.GetResourcedElementValue(priFn, info.Name, xml, "Logo", "", out strErr));

                info.PublisherDisplayName = GenLib.GetResourcedElementValue(priFn, info.Name, xml, "PublisherDisplayName", "", out strErr);
                info.SplashScreenFn = getFnWithScaleFromZip(appxFn, GenLib.GetResourcedElementValue(priFn, info.Name, xml, "SplashScreen", "Image", out strErr));

                // define screenshot
                if (info.Screenshots.Count == 0)
                {
                    string screenshotFn;
                    GenLib.FindFilenameInZip(appxFn, ".*screenshot.*[png|jpg]", out screenshotFn, out strErr);
                    info.AddScreenshot(screenshotFn != "" ? screenshotFn : info.SplashScreenFn, "Screenshot");
                }

                // define features
                if (info.Features.Count == 0)
                {
                    info.Features.Add("Enterprise Windows 8 App");
                }

                // assign whether this package needs updating
                info.NeedsUpdate = checkNeedsUpdate(info.FamilyName, info.Version);

                blnOK = true;
            }
            else
                logErr("Error extracting appx file '{0}' from appxFn '{1}' : {2}", "appxmanifest.xml", appxFn, strErr);

            return blnOK;
        }


        private bool checkNeedsUpdate(string pkgFamilyName, string appxVersionNum)
        {
            bool needsUpdate = false;

            Package pkg = findPackageByFamilyName(_allInstalledPackages, pkgFamilyName);
            if (pkg != null)
            {
                // found this package to already be installed - compare the version num with the appx version
                string pkgVersion = GenLib.GetVersionString(pkg.Id.Version);
                if (pkgVersion.Equals(appxVersionNum))
                    logInfo("App '{0}' is up to date with version {0}", appxVersionNum);
                else
                {
                    logInfo("Determined that app '{0}' is out of date; installed ver = {1} BUT appx version = {2}", pkgFamilyName, pkgVersion, appxVersionNum);
                    needsUpdate = true;
                }
            }

            return needsUpdate;
        }


        private string getFnWithScaleFromZip(string zipFn, string origFn)
        {
            string newFn = origFn;
            string foundFn;
            string errStr;

            GenLib.FindFilenameInZip(zipFn, origFn.Replace("\\","/"), out foundFn, out errStr);
            if (string.IsNullOrEmpty(foundFn))
            {
                // could not find origFn - attempt to find with *.scale-100 appended to filename prefix
                string testFn = string.Format(@"{0}/{1}.scale-100{2}", Path.GetDirectoryName(origFn), Path.GetFileNameWithoutExtension(origFn), Path.GetExtension(origFn));
                GenLib.FindFilenameInZip(zipFn, testFn, out foundFn, out errStr);
                if (!string.IsNullOrEmpty(foundFn))
                    newFn = foundFn;
            }

            return newFn;
        }

        private bool getStoreAppInfo(string storeID, ref PackageInfo app)
        {
            bool blnOK = false;
            string data;
            XmlNodeList nodes;

            try
            {
                logInfo("Attempting to download store info for app {0}, storeID {1}...", app.DisplayName, storeID);
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://next-services.apps.microsoft.com/browse/6.3.9600-0/776/en-US_en-US.en-US.en/c/US/cp/10005001/Apps/" + storeID);

                using (HttpWebResponse webresponse = (HttpWebResponse)request.GetResponse())
                {
                    if (webresponse.StatusCode == HttpStatusCode.OK)
                    {
                        using (StreamReader sr = new StreamReader(webresponse.GetResponseStream())) //, responseEncoding))
                        {
                            data = sr.ReadToEnd();
                        }

                        XmlDocument doc = new XmlDocument();
                        doc.LoadXml(data);

                        // logoFn
                        app.LogoFn = getStoreElement(doc, "Logo");

                        // screenshot filenames
                        nodes = doc.SelectNodes("//Ss/U");
                        XmlNodeList nodes2 = doc.SelectNodes("//Ss/Cap");
                        for (int i = 0; i < nodes.Count; i++)
                        {
                            XmlElement filenameEl = (XmlElement)nodes[i];
                            XmlElement descripEl = (XmlElement)nodes2[i];
                            app.AddScreenshot(filenameEl.InnerText, descripEl.InnerText);
                        }

                        // features
                        nodes = doc.GetElementsByTagName("Dbp");
                        foreach (XmlElement node in nodes)
                        {
                            app.Features.Add(node.InnerText);
                        }
                        if (app.Features.Count == 0)
                            app.Features.Add("Unknown");

                        // splashScreenFn
                        app.SplashScreenFn = getStoreElement(doc, "//Img/U");

                        // description
                        app.Description = getStoreElement(doc, "D");

                        // displayName
                        app.DisplayName = getStoreElement(doc, "T");

                        // FamilyName
                        app.FamilyName = getStoreElement(doc, "Pfn");

                        // Name
                        string[] tokens = app.FamilyName.Split('_');
                        if (tokens.Length >= 2)
                            app.Name = tokens[0];

                        // publisher
                        app.PublisherDisplayName = getStoreElement(doc, "Dev");

                        // group
                        if (string.IsNullOrEmpty(app.GroupName))
                            app.GroupName = getStoreElement(doc, "//C/N");

                        // create date
                        app.CreateDate = getStoreElement(doc, "Lud");

                        logInfo("Downloaded store info for store app {0}: logoFn='{1}', num-screenshots='{2}', description='{3}', displayName='{4}', familyName='{5}', name='{6}'",
                            storeID, app.LogoFn, app.Screenshots.Count, app.Description.Substring(0, Math.Min(app.Description.Length, 20)), app.DisplayName, app.FamilyName, app.Name);
                        blnOK = true;
                    }
                }
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception while downloading store info for app {0} in getStoreAppImageFn: {1}", storeID, ex.Message);
            }

            return blnOK;
        }


        private string getStoreElement(XmlDocument doc, string tagName)
        {
            XmlNodeList nodes;

            if (tagName.IndexOf('/') != -1)
                nodes = doc.SelectNodes(tagName);
            else
                nodes = doc.GetElementsByTagName(tagName);

            if (nodes.Count > 0)
                return nodes[0].InnerText;
            else
                return "";
        }

        /// <summary>
        /// CorpCatalog.xml structure:
        /// 
        /// <ApplicationGroup name="GroupName" description="Description of group">
        ///     <Application>
        ///         <Name>App Name</Name>
        ///         <ShortDescription>This description is shown directly below the name of the app</ShortDescription>
        ///         <Logo><full path and filename to 150x150 image</Logo>
        ///         <HyperLink>corpstore://startWin32App?exeName=<full path to exe>&arg1=<optional argument>&arg2=<opt argument>&arg3=<opt argument></HyperLink>
        ///         <HyperLink>corpstore://startEnterpriseApp?appName=<substring of app name to start>&appxFn=<full path to *.appx file of package></HyperLink>
        ///     </Application>
        /// </ApplicationGroup>
        /// ...
        /// 
        /// commonCatalog.json structure:
        /// [
        ///    {
        ///        "Name": "Store Apps for VA", 
        ///        "Description": "These are store apps used for general purpose within the VA", 
        ///        "Applications": [
        ///            {
        ///                "Name": "Songza", 
        ///                "MSStoreID": "85e49bd7-c125-40f3-8d14-04d77f3cb6d3", 
        ///            },
        ///            {
        ///                "Name": "Angry Birds", 
        ///                "MSStoreID": "8ece2571-91e0-4f2f-b7e5-b0b7944ced2d", 
        ///            },
        ///            {
        ///                "Name": "Cartoon Puzzles", 
        ///                "MSStoreID": "8ece2571-91e0-4f2f-b7e5-b0b7944ced2d", 
        ///            },
        ///        ]
        ///    },
        ///    {
        ///        "Name": "Win32 Apps for VA", 
        ///        "Description": "These are legacy win32 applications used by the VA", 
        ///        "Applications": [
        ///            {
        ///                "Name": "\windows\system32\Notepad.exe", 
        ///                "Arguments": "",
        ///            },
        ///            {
        ///                "Name": "\windows\cmd.exe",
        ///                "Arguments": "",
        ///            }
        ///        ]
        ///    },
        ///    
        ///]
        /// </summary>
        /// <param name="destFn"></param>
        /// <param name="storeGroups"></param>
        /// <param name="imagePath"></param>
        /// <param name="VAPackages"></param>
        /// <returns></returns>
        private bool createCorpCatalog(string destFn, PackageInfoGroupList combinedGroups)
        {
            bool blnOK = false;
            List<PackageInfoGroup> CorpGroups = new List<PackageInfoGroup>();

            // attempt to load raw string from commonCatalog.json file
            try
            {
                // now serialize combined groups to JSON and write to CorpCatalog.xml file
                File.WriteAllText(destFn, JsonConvert.SerializeObject(combinedGroups, Newtonsoft.Json.Formatting.Indented));
                logInfo("Wrote all text to catalog file '{0}' OK!", destFn);
                blnOK = true;
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in createCorpCatalog: {0}", ex.Message);
            }

            return blnOK;
        }


        private PackageInfoGroupList combineAppGroups(PackageInfoGroupList installedApps, PackageInfoGroupList commonApps)
        {
            PackageInfoGroupList newGroups = installedApps.Clone();

            logInfo("At top of combineAppGroups");
            try
            {
                // move over all settings contained in our commonApps into our new combined app group list
                newGroups.Settings = commonApps.Settings;

                foreach (PackageInfoGroup group in commonApps.Groups)
                {
                    PackageInfoGroup foundGroup = newGroups.Groups.Where(g => g.Name.Equals(group.Name, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
                    if (foundGroup != null)
                    {
                        // group already exists - add all apps in VAGroup into newGroups
                        logInfo("Found existing group {0} to combine {1} apps with", foundGroup.Name, group.Applications.Count);
                        foreach (PackageInfo app in group.Applications)
                        {
                            // app is not yet installed - add it to our master list
                            foundGroup.AddApplication(app);

                            // add this common app to our featured list if it has been marked as featured
                            if (app.IsFeatured)
                            {
                                newGroups.AddFeaturedApp(app);
                                logInfo("Adding app '{0}' as a featured app since it is marked as 'IsFeatured=true' in commonCatalog.json", app.DisplayName);
                            }
                        }
                    }
                    else
                    {
                        // group does not yet exist - add it 
                        logInfo("Group {0} does not exist in combined set of groups - adding it", group.Name);
                        newGroups.Groups.Add(group);

                        // now scan all packages in this new group to make sure they are considered in our feature list and so that we can augment any installed info with this common info
                        List<PackageInfo> appsToRemove = new List<PackageInfo>();
                        foreach (PackageInfo app in group.Applications)
                        {
                            // add this app to our featured list if it has been marked as featured
                            if (app.IsFeatured)
                            {
                                newGroups.AddFeaturedApp(app);
                                logInfo("Adding app '{0}' as a featured app since it is marked as 'IsFeatured=true' in commonCatalog.json", app.DisplayName);
                            }
                        }

                        // now remove marked apps from this group before moving to next group
                        for (int i = 0; i < appsToRemove.Count; i++)
                            group.Applications.Remove(appsToRemove[i]);
                    }
                }

                newGroups.SortGroupsByName();
            }
            catch (Exception ex)
            {
                logErr("Unhandled exception in combineAppGroups: {0}, stack = {1}", ex.Message, ex.StackTrace.ToString());
                newGroups = null;
            }

            logInfo("At bottom of combineAppGroups");
            return newGroups;
        }


        private Package findPackageByFamilyName(List<Package> allPackages, string familyName)
        {
            return allPackages.FirstOrDefault(p => p.Id.FamilyName.Equals(familyName, StringComparison.OrdinalIgnoreCase));
        }

        private Package findPackageByAppName(List<Package> allPackages, string appName)
        {
            return allPackages.FirstOrDefault(p => p.Id.Name.Equals(appName, StringComparison.OrdinalIgnoreCase));
        }

        private void logInfo(string msg, params object[] parms)
        {
            if (_log != null)
                _log.LogInfo(msg, parms);
        }


        private void logErr(string msg, params object[] parms)
        {
            if (_log != null)
                _log.LogErr(msg, parms);
        }



    }
}


//<Emr>
//  <Pt>
//    <I>85e49bd7-c125-40f3-8d14-04d77f3cb6d3</I>
//    <R>33452925-cbe8-4ce5-b007-6f90fab7b9d8</R>
//    <B>33452925-cbe8-4ce5-b007-6f90fab7b9d8</B>
//    <Pfn>Songza.Songza_zdadk7tk93rcp</Pfn>
//    <L>en-us</L>
//    <T>Songza</T>
//    <Wr>16</Wr>
//    <Ico>33452925-cbe8-4ce5-b007-6f90fab7b9d8/Icon.289385.png</Ico>
//    <Bg>#E5F2FF</Bg>
//    <Fg>dark</Fg>
//    <Sr>4.4</Sr>
//    <Src>1225</Src>
//    <Cs>$</Cs>
//    <Cc>USD</Cc>
//    <P>0.00</P>
//    <C>
//      <I>8</I>
//      <N>Music &amp; Video</N>
//    </C>
//    <Sc>
//      <I>80</I>
//      <N>Music</N>
//    </Sc>
//    <Typ>1</Typ>
//    <Acc>false</Acc>
//    <Dca>false</Dca>
//    <Try>false</Try>
//    <Lud>2014-01-20T08:13:02.953Z</Lud>
//    <Ats>
//      <At>
//        <N>Songza</N>
//        <Bg>#E5F2FF</Bg>
//        <Fg>dark</Fg>
//        <Mai>App</Mai>
//        <Logo>33452925-cbe8-4ce5-b007-6f90fab7b9d8/AppTile.2.289385.290811.png</Logo>
//        <Imgs>
//          <Img>
//            <T>1</T>
//            <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/AppTile.1.289385.290811.png</U>
//          </Img>
//          <Img>
//            <T>3</T>
//            <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/AppTile.3.289385.290811.png</U>
//          </Img>
//        </Imgs>
//      </At>
//    </Ats>
//  </Pt>
//  <S>1</S>
//  <D>Working? Relaxing? At the gym? Songza plays you the right music at the right time.</D>
//  <Dbps>
//    <Dbp>Free, with no listening limits.</Dbp>
//    <Dbp>Stream thousands of original playlists handmade by music experts.</Dbp>
//    <Dbp>Let Songza's 'Music Concierge' find the right music for your moments.</Dbp>
//    <Dbp>Save your favorite playlists &amp; share them via Facebook, Twitter or email.</Dbp>
//  </Dbps>
//  <Ud>Changes:

//Windows 8.1 Compatible!</Ud>
//  <Src>1225</Src>
//  <Om>1688862745165825</Om>
//  <Oc>true</Oc>
//  <Is>true</Is>
//  <Bsl>English (United States)</Bsl>
//  <Sls>
//    <Sl>English (United States)</Sl>
//  </Sls>
//  <Dev>Songza</Dev>
//  <DevI>844428041089983</DevI>
//  <V>566614372413215718</V>
//  <Cr>Copyright 2013, Songza</Cr>
//  <Ws>http://songza.com/</Ws>
//  <Sws>mailto:support@songza.com</Sws>
//  <Pu>http://songza.com/page/privacy/</Pu>
//  <Ds>381</Ds>
//  <Sas2>
//    <Sa2>
//      <An>x86</An>
//    </Sa2>
//    <Sa2>
//      <An>x64</An>
//    </Sa2>
//    <Sa2>
//      <An>ARM</An>
//    </Sa2>
//  </Sas2>
//  <Cs>
//    <C>APPX_CAPABILITY_INTERNET_CLIENT</C>
//  </Cs>
//  <Sss>
//    <Ss>
//      <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/Screenshot.289385.100000.jpg</U>
//      <Cap>Find the perfect music for your Friday Night.</Cap>
//    </Ss>
//    <Ss>
//      <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/Screenshot.289385.1000000.jpg</U>
//      <Cap>Songza intelligently selects situations based on the day and time.</Cap>
//    </Ss>
//    <Ss>
//      <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/Screenshot.289385.1000001.jpg</U>
//      <Cap>View your recent and favorite playlists right from the hub.</Cap>
//    </Ss>
//    <Ss>
//      <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/Screenshot.289385.1000002.jpg</U>
//      <Cap>The Concierge will present you with three expertly curated playlists to choose from.</Cap>
//    </Ss>
//    <Ss>
//      <U>33452925-cbe8-4ce5-b007-6f90fab7b9d8/Screenshot.289385.1000003.jpg</U>
//      <Cap>Navigate quickly and control playback from anywhere in the app.</Cap>
//    </Ss>
//  </Sss>
//</Emr>
